import collections
import logging
import traceback
import typing

from google.protobuf.internal.enum_type_wrapper import EnumTypeWrapper

from coin.base.timestamp import get_timestamp
from coin.proto.coin_request_pb2 import (
    ApplicationInfoProto,
    StrategyInfoProto)
from coin.proto.coin_strategy_pb2 import StrategyLog
from coin.util.queue.tools.kafka_topic import parse_kafka_topic
from coin.support.alarm.logic.alarm_generator import StrategyTimestamp
from coin.support.alarm.logic.factory import alarm_ctor_info_map
import xunkemgmt.service.alarm_service_pb2 as as_pb2


def get_alarm_config(strat_info: StrategyInfoProto, alarm_type: EnumTypeWrapper):
  alarm_ctor_info = alarm_ctor_info_map[alarm_type]
  if alarm_type == as_pb2.NO_ORDER_EVENT_ALARM:
    field = 'threshold_minute'
    params = {field: None}
    if strat_info.business_unit == 'Prex':
      params[field] = 5
    return alarm_ctor_info.config_type(**params)
  else:
    return alarm_ctor_info.config_type()


class AlarmDetector(object):
  def __init__(self,
               *,
               strategy_info: StrategyInfoProto,
               application_infos: typing.List[ApplicationInfoProto],
               topics: typing.List[str],
               alarm_configs: typing.Dict[EnumTypeWrapper, typing.Any],
               current_ts: int=None,
               ignored_alarm_types: typing.Optional[typing.List[int]]=None):
    self._strategy_info = strategy_info
    self._machines = set()
    self._ignored_alarm_types = ignored_alarm_types
    current_ts = get_timestamp() if current_ts is None else current_ts
    self._last_ts = StrategyTimestamp(current_ts=current_ts)
    self._alarm_msgs = collections.defaultdict(list)
    self._alarm_handlers = {}
    for alarm_type, alarm_config in alarm_configs.items():
      alarm_ctor_info = alarm_ctor_info_map[alarm_type]
      if alarm_type == as_pb2.ABNORMAL_APPLICATION_ALARM:
        ctor_params = [alarm_config, self._filter_applications(application_infos)]
      elif alarm_type == as_pb2.RETIRED_STRATEGY_ALARM:
        ctor_params = [alarm_config, strategy_info]
      elif alarm_type == as_pb2.TOPIC_NOT_FOUND_ALARM:
        ctor_params = [alarm_config, self._filter_topics(topics)]
      else:
        ctor_params = [alarm_config]
      self._alarm_handlers[alarm_type] = alarm_ctor_info.ctor_type(*ctor_params)

  def _filter_alarms(self, alarms: typing.List[as_pb2.AlarmValueProto]
  ) -> typing.List[as_pb2.AlarmValueProto]:
    if self._ignored_alarm_types is not None:
      alarms = [alarm for alarm in alarms
                if alarm.alarm_type not in self._ignored_alarm_types]
    alarm_dict = collections.defaultdict(list)
    filtered = []
    for alarm in alarms:
      alarm_dict[alarm.alarm_type].append(alarm)
    # if not launched, only report TOPIC_NOT_FOUND_ALARM
    if as_pb2.TOPIC_NOT_FOUND_ALARM in alarm_dict:
      filtered.extend(alarm_dict[as_pb2.TOPIC_NOT_FOUND_ALARM])
    # if launched but deactivated, only report RETIRED_STRATEGY_ALARM
    elif not self._strategy_info.active:
      filtered.extend(alarm_dict.get(as_pb2.RETIRED_STRATEGY_ALARM, []))
    # if launched & active but killed, only report NO_STRATEGY_LOG_ALARM
    elif as_pb2.NO_STRATEGY_LOG_ALARM in alarm_dict:
      filtered.extend(alarm_dict[as_pb2.NO_STRATEGY_LOG_ALARM])
    else:
      filtered.extend(alarms)
    return filtered

  def _filter_applications(self, application_infos: typing.List[ApplicationInfoProto]):
    return [elem for elem in application_infos
            if elem.strategy_name == self._strategy_info.strategy_name]

  def _filter_topics(self, topics: typing.List[str]) -> typing.List[str]:
    filtered = []
    for topic in topics:
      strategy_name = parse_kafka_topic(topic)[1].strategy_name
      if strategy_name == self._strategy_info.strategy_name:
        filtered.append(topic)
    return filtered

  def _update_current_ts(self, ts):
    if ts > self._last_ts.current_ts:
      self._last_ts.current_ts = ts

  def _update_log_ts(self, ts, pb):
    if ts > self._last_ts.last_log_ts:
      self._last_ts.last_log_ts = ts
    if pb.HasField('pnl_balance') and ts > self._last_ts.last_pnl_balance_ts:
      self._last_ts.last_pnl_balance_ts = ts
    if pb.HasField('og_log') and pb.og_log.HasField('event') and \
       ts > self._last_ts.last_order_event_ts:
      self._last_ts.last_order_event_ts = ts

  def update_application_infos(
      self, application_infos: typing.List[ApplicationInfoProto]):
    self._alarm_handlers[as_pb2.ABNORMAL_APPLICATION_ALARM].update_application_infos(
        self._filter_applications(application_infos))

  def update_strategy_info(self, strategy_info: StrategyInfoProto):
    assert strategy_info.HasField('strategy_name') and \
           strategy_info.strategy_name == self._strategy_info.strategy_name
    self._strategy_info = strategy_info
    self._alarm_handlers[as_pb2.RETIRED_STRATEGY_ALARM].update_strategy_info(
        strategy_info)

  def update_topics(self, topics: typing.List[str]):
    self._alarm_handlers[as_pb2.TOPIC_NOT_FOUND_ALARM].update_topics(
        self._filter_topics(topics))

  def update_alarm_configs(
      self, alarm_configs: typing.Dict[EnumTypeWrapper, typing.Any]):
    for alarm_type, alarm_config in alarm_configs.items():
      self._alarm_handlers[alarm_type].update_alarm_config(alarm_config)

  def handle(self, ts: int, machine: str, pb: StrategyLog):
    try:
      self._update_current_ts(ts)
      self._update_log_ts(ts, pb)
      self._machines.add(machine)
      for alarm in self._alarm_handlers.values():
        alarm.handle_strategy_timestamp(self._last_ts)
        alarm.handle_strategy_log(ts, machine, pb)
    except Exception as e:
      logging.error(
          'Fail to handle %s' % self._strategy_info.strategy_name)
      logging.error('%s, %s' % (ts, pb))
      logging.error(traceback.format_exc())

  def get_alarm(self, ts: typing.Optional[int]=None) -> as_pb2.AlarmProto:
    ts = ts if ts is not None else self._last_ts.current_ts
    self._update_current_ts(ts)
    for alarm in self._alarm_handlers.values():
      alarm.handle_strategy_timestamp(self._last_ts)
    alarm_values = []
    alarm_info = as_pb2.AlarmInfoProto(
        strategy_id=self._strategy_info.strategy_id,
        strategy_name=self._strategy_info.strategy_name,
        strategy_group=self._strategy_info.strategy_group,
        trader=self._strategy_info.trader,
        business_unit=self._strategy_info.business_unit,
        machines=sorted(list(self._machines)))
    for handler in self._alarm_handlers.values():
      alarm_values.extend(handler.get_alarms())
    alarm_values = self._filter_alarms(alarm_values)
    return as_pb2.AlarmProto(alarm_info=alarm_info, alarm_values=alarm_values)
